/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package hellfire.system.graphics;

import hellfire.platform.graphics.PlatformShaders;
import hellfire.system.R;
import hellfire.system.R.drawable;
import hellfire.system.engine.Engine;
import hellfire.system.engine.Main;
import hellfire.system.engine.Task;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;
import java.util.Vector;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;

import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.opengl.Matrix;
import android.util.Log;

public class GLRenderer implements GLSurfaceView.Renderer 
{
   
    public static float ASP = 1;
    public static boolean isSurfaceCreated = false;
    
    private final float[] matrix = new float[16];
    
    private int[] textures;
    private Map<String, Integer> 	shaderCodeToID = new TreeMap<String, Integer>();
    private Map<Integer, Integer> 	programIdxToID = new TreeMap<Integer, Integer>();
    private HashMap<Integer, Integer> 	rtt; //resource to tex
	private Context context; 
	private LinkedList<Task> tasks;
	private Stack<float[]> matrixStack = new Stack<float[]>();
	
	public GLRenderer(Context context) 
	{
		this.context = context;
		tasks = new LinkedList<Task>();
	}
	
	public static void setScissorBox(GLVec2 pos, GLVec2 dim)
	{
		GLRelVec2 relPos = new GLRelVec2(pos);
		GLRelVec2 relDim = new GLRelVec2(dim);
		
		int x = (int) ((((relPos.x() - relDim.x()) + 1) / 2)*Main.UI.getWidth());
		int y = (int) ((((relPos.y() - relDim.y()) + 1) / 2)*Main.UI.getHeight());
		int w = (int) (relDim.x()*Main.UI.getWidth());
		int h = (int) (relDim.y()*Main.UI.getHeight());
		//Log.d("R", x + " " + y + " " + w + " " + h);
		
		GLES20.glScissor(x,y,w,h);
	}
	
    @Override
    public void onSurfaceCreated(GL10 unused, EGLConfig config) 
    {
    	loadShaders();
    	GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        Class<drawable> c = R.drawable.class;
        int numDrawables = c.getFields().length;
        textures = new int[numDrawables];
        rtt = new HashMap<Integer, Integer>();
        for(final Field f : c.getFields())
        {
        	try 
        	{
				final int value = f.getInt(f);
				
				Task t = new Task(null) 
				{
					public void run() 
					{
						try
						{
							loadGLTexture(value);
						}
						catch (Exception e) 
						{
							Log.d("R", "Failed to load texture to GPU: " + f.getName());
							e.printStackTrace();
						}
					}
				};
				runTask(t);
			} catch (Exception e) 
			{
				Log.d("R", "Failed to load texture resource " + f.getName());
				e.printStackTrace();
			}
        }
        //GLES20.glEnable(GLES20.GL_TEXTURE_2D);			
        GLES20.glEnable(GLES20.GL_BLEND); 
        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
        GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
        setScissorBox(new GLRelVec2(0,0), new GLRelVec2(1,1));
        isSurfaceCreated = true;
        Log.d("R", "surface created");
    }

    public int getShaderProgram(String[] shaderPair)
    {
    	for(int i = 0; i < Shaders.progList.length; i++)
    	{
    		if(shaderPair == Shaders.progList[i])
    		{
    			return programIdxToID.get(i);
    		}
    	}
    	for(int i = 0; i < PlatformShaders.progList.length; i++)
    	{
    		if(shaderPair == PlatformShaders.progList[i])
    		{
    			return programIdxToID.get(i+Shaders.progList.length);
    		}
    	}
    	return 0;
    }
    
    private void loadShaders()
    {
    	for(int i = 0; i < Shaders.progList.length + PlatformShaders.progList.length; i++)
    	{
    		String[] program = i < Shaders.progList.length ? Shaders.progList[i] : PlatformShaders.progList[i-Shaders.progList.length];
    		String vertexShader = program[0];
    		String fragmentShader = program[1];
    		int vertexShaderID = GLRenderer.loadShader(GLES20.GL_VERTEX_SHADER, vertexShader);
            int fragmentShaderID = GLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);
            if(vertexShaderID == 0 || fragmentShaderID == 0)
            {
            	Log.d("R", "Failed to load shaders for program " + i);
            }
            shaderCodeToID.put(vertexShader, vertexShaderID);
            shaderCodeToID.put(fragmentShader, fragmentShaderID);
            int programID = loadProgram(vertexShaderID, fragmentShaderID);
            if(programID == 0)
            {
            	Log.d("R", "Failed to link program " + i);
            }
            programIdxToID.put(i, programID);
    	}
    }
    
    public static void pushMatrix(float[] matrix)
    {
    	float[] copy = matrix.clone();
    	Main.UI.renderer.matrixStack.push(copy);
    }
    
    public static void popMatrix(float[] matrix)
    {
    	float[] popped = Main.UI.renderer.matrixStack.pop();
    	for(int i = 0; i < matrix.length; i++)
    		matrix[i] = popped[i];
    }
    
    private static int loadProgram(int vertexShaderID, int fragmentShaderID)
    {
    	int program = GLES20.glCreateProgram();             // create empty OpenGL Program
    	if(program == 0) return 0;
        GLES20.glAttachShader(program, vertexShaderID);   // add the vertex shader to program
        GLES20.glAttachShader(program, fragmentShaderID); // add the fragment shader to program
        GLES20.glLinkProgram(program);   
        final int[] linkStatus = new int[1];
        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
        if (linkStatus[0] == 0)
        {
        	Log.d("R", "Some shaders were not linked");
            GLES20.glDeleteProgram(program);
           	return 0;
        }
        return program;
    }
    
    private static int loadShader(int type, String shaderCode)
    {
       int shader = GLES20.glCreateShader(type);
       if(shader == 0) return 0;
       GLES20.glShaderSource(shader, shaderCode);
       GLES20.glCompileShader(shader);
       final int[] compileStatus = new int[1];
       GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
       if (compileStatus[0] == 0)
       {
           GLES20.glDeleteShader(shader);
           Log.d("R", "Shader failed to compile: ");
           Log.d("R", shaderCode);
           return 0;
       }
       return shader;
    }
    
    
    @Override
    public void onDrawFrame(GL10 unused) 
    {
    	GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

        Matrix.setIdentityM(matrix, 0);
        matrix[0] = 1/ASP;
        try
        {
      
	        synchronized(Engine.Lock)
	        {
	        	if(Engine.Freeze)
	        		return;
	        	if(Main.Engine.getCurrentGameScreen() != null)
	        		Main.Engine.getCurrentGameScreen().draw(matrix);
	        	GLES20.glFlush();
	        	if(!tasks.isEmpty())
	            	tasks.pollFirst().run();
	        }
        }
        catch(Exception e)
        {
        	StringWriter sw = new StringWriter();
			PrintWriter pw = new PrintWriter(sw);
			e.printStackTrace(pw);
			pw.flush();
			synchronized(Engine.Lock)
	        {
				Engine.Freeze = true;
		        Main.MessageBoxStrict("Graphics crash:\n" + sw.toString(), new OnClickListener() 
				{
					public void onClick(DialogInterface dialog, int which) 
					{
						Main.Engine.terminate();
					}
				}); 
	        }
			e.printStackTrace();
        }
        //Log.d("R", "GPU iteration");
        Main.EngineThread.interrupt();
        //Log.d("R", "GLSL version " + GLES20.glGetString(GLES20.GL_SHADING_LANGUAGE_VERSION));
    }

    public void runTask(Task t)
    {
    	tasks.add(t);
    }
    
    @Override
    public void onSurfaceChanged(GL10 unused, int width, int height) 
    {
        float ratio = (float) width / height;
        if(ASP == 1)
        	ASP = ratio;
        GLES20.glViewport(0, 0, width, height);
        //Log.d("R", "surface changed: [" + width + "," + height + "]"); 
    }

     

	public static void checkGLError(String glOperation)
	{
	   int error;
	   while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) 
	   {
	       throw new RuntimeException(glOperation + ": glError " + error);
	   }
	}
    
	public void loadGLTexture(int resourceId) 
	{
		Bitmap bitmap = BitmapFactory.decodeResource(this.context.getResources(), resourceId);
		loadGLTexture(bitmap, resourceId);
	}
    
    private boolean isValidGLsize(int width, int height)
    {
    	return nextPowerOfTwo(width) == width && nextPowerOfTwo(height) == height;
    }
    
    public void loadGLTexture(Bitmap bitmap, int resourceId)
 	{
 		try
 		{
 			Bitmap newBitmap = null;
	    	int w = bitmap.getWidth();
	    	int h = bitmap.getHeight();
 			if(!isValidGLsize(w, h))
	    	{
	    		newBitmap = Bitmap.createBitmap(nextPowerOfTwo(w),nextPowerOfTwo(h), bitmap.getConfig());
	    		Canvas canvas = new Canvas(newBitmap);
		 		
	    		//android.graphics.Matrix flipHorizontalMatrix = new android.graphics.Matrix();
	    		//flipHorizontalMatrix.setScale(1,-1);
	    	    //flipHorizontalMatrix.postTranslate(0, bitmap.getHeight());
	    		
	    		//canvas.drawBitmap(bitmap, flipHorizontalMatrix, null); 
		 		canvas.drawBitmap(bitmap, 0, 0, null);
	    		bitmap.recycle();
	    	}
 			else newBitmap = bitmap;
 			GLES20.glGenTextures(1, textures, rtt.size());
 			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[rtt.size()]);
	 		rtt.put(resourceId, textures[rtt.size()]);
	 		GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
	 		GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
	 		GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
	 		GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
	 	    //newBitmap.
	 		GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, newBitmap, 0);
	 	    newBitmap.recycle();
 		}
 	    catch(Exception e)
 	    {
 	    	rtt.put(resourceId, 0);
 	    	Log.d("R", "No memory for texImage2D");
 	    	e.printStackTrace();
 	    }
 		Log.d("R", "loaded texture: " + resourceId);
 	}
    
    public static int nextPowerOfTwo(int num)
 	{
 		for(int i = 0; true; i++)
 		{
 			double x = Math.pow(2, (double)i);
 			if(x >= num) return (int)x;
 		}
 	}
    
    public int ResourceIdToTexture(int resourceId)
	{
		Integer ans = rtt.get(resourceId);
		if(ans != null) return ans.intValue();
		return -1;
	}
    
}